// PlanetView3D.cs
//
// Copyright (c) 2007 Mike Strobel
//
// This source code is subject to the terms of the Microsoft Reciprocal License (Ms-RL).
// For details, see <http://www.opensource.org/licenses/ms-rl.html>.
//
// All other rights reserved.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using System.Xml.Linq;

using Microsoft.Practices.ServiceLocation;

using Supremacy.Client;
using Supremacy.Entities;
using Supremacy.Resources;
using Supremacy.Types;
using Supremacy.Universe;

namespace Supremacy.UI
{
    public sealed class PlanetView3D : FrameworkElement, IAnimationsHost
    {
        #region Constants
        private const double BaseDimension = 96.0;
        #endregion

        #region Fields
        private static readonly Dictionary<PlanetType, List<CachedBitmap>> s_atmospheres;
        private static readonly Dictionary<string, CachedBitmap> s_customAtmospheres;
        private static readonly Dictionary<string, CachedBitmap> s_customMaterials;
        private static readonly Dictionary<PlanetType, List<CachedBitmap>> s_materials;
        private static readonly Dictionary<MoonShape, CachedBitmap> s_moonImages;
        private static readonly string s_planetSizeTypeFormat;

        private static IClientContext s_clientContext;

        private readonly AnimationClock _animationClock;
        private readonly DoubleAnimation _axisAnimation;
        private readonly AxisAngleRotation3D _axisRotation;
        private readonly OrthographicCamera _camera;
        private readonly RotateTransform3D _cameraAngle;

        private readonly Grid _grid;
        private readonly TextBlock _label;
        private readonly ModelVisual3D _light;
        private readonly DiffuseMaterial _material;
        private readonly ModelVisual3D _model;
        private readonly StackPanel _moons;
        private readonly Image _overlay;
        private readonly RotateTransform3D _rotation;
        private readonly ScaleTransform3D _scale;
        private readonly Sphere _sphere;
        private readonly ToolTip _toolTip;
        private readonly string _toolTipText;

        private readonly Transform3DGroup _transforms;
        private readonly Viewport3D _view3d;
        #endregion

        #region Constructors
        static PlanetView3D()
        {
            FocusVisualStyleProperty.OverrideMetadata(
                typeof(PlanetView3D),
                new FrameworkPropertyMetadata(
                    null,
                    (d, baseValue) => null));

            XDocument customPlanetsXml = XDocument.Load(ResourceManager.GetResourcePath(@"Resources\Data\CustomPlanets.xml"));

            var customPlanets = from customPlanet in customPlanetsXml.Element("CustomPlanets").Elements("CustomPlanet")
                                let name = customPlanet.Attribute("Name").Value
                                let imageSource = new CachedBitmap(
                                    new BitmapImage(
                                        ResourceManager.GetResourceUri(
                                            "Resources/Images/Planets/" + customPlanet.Attribute("Texture").Value)),
                                        BitmapCreateOptions.None,
                                        BitmapCacheOption.OnLoad)
                                let atmosphereSource = new CachedBitmap(
                                    new BitmapImage(
                                        ResourceManager.GetResourceUri(
                                            "Resources/Images/Atmospheres/" + customPlanet.Attribute("Atmosphere").Value)),
                                        BitmapCreateOptions.None,
                                        BitmapCacheOption.OnLoad)
                                select new { Name = name, ImageSource = imageSource, AtmosphereSource = atmosphereSource };

            s_customMaterials = new Dictionary<string, CachedBitmap>();
            s_customAtmospheres = new Dictionary<string, CachedBitmap>();
            s_materials = new Dictionary<PlanetType, List<CachedBitmap>>();
            s_atmospheres = new Dictionary<PlanetType, List<CachedBitmap>>();
            s_moonImages = new Dictionary<MoonShape, CachedBitmap>();
            s_planetSizeTypeFormat = ResourceManager.GetString("PLANET_SIZE_TYPE_FORMAT");

            foreach (var customPlanet in customPlanets)
            {
                s_customMaterials.Add(customPlanet.Name, customPlanet.ImageSource);
                s_customAtmospheres.Add(customPlanet.Name, customPlanet.AtmosphereSource);
            }

            foreach (PlanetType type in Enum.GetValues(typeof(PlanetType)))
            {
                if (type == PlanetType.Asteroids)
                    continue;
                s_materials[type] = new List<CachedBitmap>();
                s_atmospheres[type] = new List<CachedBitmap>();
                for (int i = 0; i < 3; i++)
                {
                    s_materials[type].Insert(i, new CachedBitmap(new BitmapImage(
                                                                     ResourceManager.GetResourceUri(
                                                                         String.Format("Resources/Images/Planets/{0}{1:00}.png", type, i + 1))),
                                                                 BitmapCreateOptions.None,
                                                                 BitmapCacheOption.OnLoad));
                    RenderOptions.SetBitmapScalingMode(s_materials[type][i], BitmapScalingMode.LowQuality);
                    s_materials[type][i].Freeze();
                    s_atmospheres[type].Insert(i, new CachedBitmap(new BitmapImage(
                                                                       ResourceManager.GetResourceUri(
                                                                           String.Format("Resources/Images/Atmospheres/{0}{1:00}.png", type, i + 1))),
                                                                   BitmapCreateOptions.None,
                                                                   BitmapCacheOption.OnLoad));
                    RenderOptions.SetBitmapScalingMode(s_atmospheres[type][i], BitmapScalingMode.LowQuality);
                    s_atmospheres[type][i].Freeze();
                }
            }

            foreach (MoonShape shape in Enum.GetValues(typeof(MoonShape)))
            {
                s_moonImages[shape] = new CachedBitmap(new BitmapImage(
                                                           ResourceManager.GetResourceUri(
                                                               String.Format("Resources/Images/Planets/Moons/{0}.png", shape))),
                                                       BitmapCreateOptions.None,
                                                       BitmapCacheOption.OnLoad);
                RenderOptions.SetBitmapScalingMode(s_moonImages[shape], BitmapScalingMode.LowQuality);
                s_moonImages[shape].Freeze();
            }
        }

        public PlanetView3D(Planet planet, StarSystem system)
        {
            double dimension = BaseDimension;
            Race targetRace;
            bool customTexture = false;

            if (system.HasColony)
            {
                targetRace = system.Colony.Inhabitants;
            }
            else
            {
                targetRace = this.ClientContext.LocalPlayerEmpire.Civilization.Race;
            }

            PlanetEnvironment environment = planet.GetEnvironment(targetRace);
            int maxPopulation = planet.GetMaxPopulation(targetRace);
            Percentage growthRate = planet.GetGrowthRate(targetRace);

            _sphere = new Sphere();
            _model = new ModelVisual3D();
            _grid = new Grid();
            _view3d = new Viewport3D();
            _overlay = new Image();
            _label = new TextBlock();
            _transforms = new Transform3DGroup();
            _scale = new ScaleTransform3D(new Vector3D(1, 1, 1));
            _axisRotation = new AxisAngleRotation3D(new Vector3D(0, 1, 0), 0);
            _rotation = new RotateTransform3D(_axisRotation, new Point3D(0, 0, 0));
            _transforms.Children.Add(_scale);
            _transforms.Children.Add(_rotation);

            _view3d.ClipToBounds = false;
            _view3d.IsHitTestVisible = false;

            _grid.RowDefinitions.Add(new RowDefinition());
            _grid.RowDefinitions.Add(new RowDefinition());
            _grid.RowDefinitions.Add(new RowDefinition());
            _grid.VerticalAlignment = VerticalAlignment.Center;

            _view3d.SetValue(Grid.RowProperty, 1);
            _overlay.SetValue(Grid.RowProperty, 1);
            _label.SetValue(Grid.RowProperty, 2);

            switch (planet.PlanetSize)
            {
                case PlanetSize.Large:
                    dimension = 64;
                    break;
                case PlanetSize.Medium:
                    dimension = 48;
                    break;
                case PlanetSize.Small:
                    dimension = 36;
                    break;
                case PlanetSize.Tiny:
                    dimension = 24;
                    break;
                case PlanetSize.Giant:
                    dimension = 80;
                    break;
                default:
                    break;
            }
            _view3d.Width = dimension;
            _view3d.Height = dimension;
            _overlay.Width = _view3d.Width + 2;
            _overlay.Height = _view3d.Height + 2;

            _toolTip = new ToolTip();
            _toolTipText = planet.Name;
            if (planet.PlanetType == PlanetType.GasGiant)
            {
                _toolTipText += "\n";
                _toolTipText += ResourceManager.GetString("PLANET_TYPE_GASGIANT");
            }
            else if (planet.PlanetType == PlanetType.Crystalline)
            {
                _toolTipText += "\n";
                _toolTipText += ResourceManager.GetString("PLANET_TYPE_CRYSTALLINE");
            }
            else
            {
                _toolTipText += "\n";
                _toolTipText += String.Format(
                    s_planetSizeTypeFormat,
                    ResourceManager.GetString("PLANET_SIZE_" + planet.PlanetSize.ToString().ToUpper()),
                    ResourceManager.GetString("PLANET_TYPE_" + planet.PlanetType.ToString().ToUpper()));
            }
            _toolTipText += "\n";
            if (environment == PlanetEnvironment.Uninhabitable)
            {
                _toolTipText += ResourceManager.GetString(
                    "PLANET_ENVIRONMENT_" + environment.ToString().ToUpper());
            }
            else
            {
                _toolTipText += ResourceManager.GetString("PLANET_ENVIRONMENT") + ": ";
                _toolTipText += ResourceManager.GetString(
                    "PLANET_ENVIRONMENT_" + environment.ToString().ToUpper());
                _toolTipText += "\n" + ResourceManager.GetString("PLANET_MAX_POPULATION")
                    + ": " + maxPopulation;
                _toolTipText += "\n" + ResourceManager.GetString("PLANET_GROWTH_RATE")
                    + ": " + growthRate;
            }
            _toolTip.Content = _toolTipText;
            _overlay.ToolTip = _toolTip;

            _label.TextAlignment = TextAlignment.Center;
            _label.Foreground =
                ((planet.PlanetType == PlanetType.Crystalline)
                    || (planet.PlanetType == PlanetType.GasGiant)
                        || (planet.PlanetType == PlanetType.Demon))
                    ? Brushes.Blue : Brushes.Green;
            _label.Margin = new Thickness(2);
            _label.FontSize = 10.0;
            _label.FontFamily = new FontFamily("Arial");
            switch (planet.PlanetType)
            {
                case PlanetType.Barren:
                    _label.Text = "J";
                    break;
                case PlanetType.Desert:
                    _label.Text = "G";
                    break;
                case PlanetType.Crystalline:
                    _label.Text = "D";
                    break;
                case PlanetType.GasGiant:
                    _label.Text = "B";
                    break;
                case PlanetType.Volcanic:
                    _label.Text = "K";
                    break;
                case PlanetType.Oceanic:
                    _label.Text = "O";
                    break;
                case PlanetType.Rogue:
                    _label.Text = "R";
                    break;
                case PlanetType.Demon:
                    _label.Text = "Y";
                    break;
                case PlanetType.Jungle:
                    _label.Text = "L";
                    break;
                case PlanetType.Terran:
                    _label.Text = "M";
                    break;
                case PlanetType.Arctic:
                    _label.Text = "P";
                    break;
                default:
                    _label.Text = "";
                    _label.Visibility = Visibility.Collapsed;
                    break;
            }

            if (s_customMaterials.ContainsKey(planet.Name))
            {
                customTexture = true;
                _material = new DiffuseMaterial(
                    new ImageBrush(s_customMaterials[planet.Name]));
            }
            else if (s_materials.ContainsKey(planet.PlanetType))
            {
                if (s_materials[planet.PlanetType].Count > planet.Variation)
                {
                    _material = new DiffuseMaterial(new ImageBrush(
                                                        s_materials[planet.PlanetType][planet.Variation]));
                }
                else
                {
                    if (s_materials[planet.PlanetType].Count > 0)
                    {
                        _material = new DiffuseMaterial(
                            new ImageBrush(s_materials[planet.PlanetType][0]));
                    }
                }
            }

            if (customTexture && s_customAtmospheres.ContainsKey(planet.Name))
            {
                _overlay.Source = s_customAtmospheres[planet.Name];
            }
            else if (s_atmospheres.ContainsKey(planet.PlanetType)
                && (s_atmospheres[planet.PlanetType].Count > planet.Variation))
            {
                _overlay.Source = s_atmospheres[planet.PlanetType][planet.Variation];
            }
            else
            {
                if (s_atmospheres.ContainsKey(PlanetType.Barren)
                    && (s_atmospheres[PlanetType.Barren].Count > 0))
                {
                    _overlay.Source = s_atmospheres[PlanetType.Barren][0];
                }
            }

            if (_material == null)
            {
                _material = new DiffuseMaterial(Brushes.White);
            }

            _material.AmbientColor = Color.FromScRgb(1.0f, 0.3f, 0.3f, 0.3f);
            _material.Color = Color.FromScRgb(1.0f, 0.7f, 0.7f, 0.7f);

            _model.Content = new GeometryModel3D(_sphere.Mesh, _material);
            _model.Transform = _transforms;
            _view3d.Children.Add(_model);

            _light = new ModelVisual3D
                     {
                         Content = new DirectionalLight(
                             Colors.White, new Vector3D(0.85, -0.15, 1))
                     };
            _view3d.Children.Add(_light);

            _moons = new StackPanel
                     {
                         Height = 16,
                         Orientation = Orientation.Horizontal,
                         Margin = new Thickness(0, 0, 0, 4),
                         HorizontalAlignment = HorizontalAlignment.Center
                     };
            _moons.SetValue(Grid.RowProperty, 0);
            for (int i = 0; i < planet.Moons.Length; i++)
            {
                int size;
                ToolTip moonToolTip = new ToolTip();
                Image moon = new Image
                             {
                                 Source = s_moonImages[planet.Moons[i].GetShape()]
                             };
                switch (planet.Moons[i].GetSize())
                {
                    case MoonSize.Small:
                        size = 8;
                        moonToolTip.Content = "Small Moon";
                        break;
                    case MoonSize.Medium:
                        size = 12;
                        moonToolTip.Content = "Medium Moon";
                        break;
                    default:
                        size = 16;
                        moonToolTip.Content = "Large Moon";
                        break;
                }
                moon.Width = moon.Height = size;
                moon.VerticalAlignment = VerticalAlignment.Center;
                moon.Margin = new Thickness(1, 0, 1, 0);
                moon.ToolTip = moonToolTip;
                _moons.Children.Add(moon);
            }

            _grid.Children.Add(_view3d);
            _grid.Children.Add(_overlay);
            _grid.Children.Add(_label);
            _grid.Children.Add(_moons);

            _cameraAngle = new RotateTransform3D(
                new AxisAngleRotation3D(new Vector3D(0, 0, 1), 15),
                new Point3D(0, 0, 0));
            _camera = new OrthographicCamera(
                new Point3D(0, 0, -1),
                new Vector3D(0, 0, 1),
                new Vector3D(0, 1, 0),
                (double)OrthographicCamera.WidthProperty.DefaultMetadata.DefaultValue)
                      {
                          NearPlaneDistance = (-1),
                          FarPlaneDistance = 1,
                          Transform = _cameraAngle
                      };
            _view3d.Camera = _camera;

            _axisAnimation = new DoubleAnimation(0, 360, new Duration(new TimeSpan(0, 0, 20)))
                             {
                                 RepeatBehavior = RepeatBehavior.Forever
                             };

            //Timeline.SetDesiredFrameRate(_axisAnimation, 24);

            _animationClock = _axisAnimation.CreateClock();

            AddVisualChild(_grid);

            Loaded += PlanetView3D_Loaded;
            Unloaded += PlanetView3D_Unloaded;
        }
        #endregion

        private void PlanetView3D_Unloaded(object sender, RoutedEventArgs e)
        {
            StopAnimations();
        }

        private void PlanetView3D_Loaded(object sender, RoutedEventArgs e)
        {
            _axisRotation.ApplyAnimationClock(AxisAngleRotation3D.AngleProperty, _animationClock);
            Loaded -= PlanetView3D_Loaded;
        }

        #region Properties
        // ReSharper disable MemberCanBeMadeStatic.Local
        private IClientContext ClientContext
        {
            get
            {
                var clientContext = s_clientContext;
                if (clientContext == null)
                {
                    var newClientContext = ServiceLocator.Current.GetInstance<IClientContext>();
                    clientContext = Interlocked.CompareExchange(ref s_clientContext, clientContext, null) ?? newClientContext;
                }
                return clientContext;
            }
        }
        // ReSharper restore MemberCanBeMadeStatic.Local

        protected override int VisualChildrenCount
        {
            get { return 1; }
        }
        #endregion

        #region IAnimationsHost Members
        public void PauseAnimations()
        {
            if (!_animationClock.IsPaused && _animationClock.Controller != null)
                _animationClock.Controller.Pause();
        }

        public void ResumeAnimations()
        {
            if (_animationClock.IsPaused && _animationClock.Controller != null)
                _animationClock.Controller.Resume();
        }

        public void StopAnimations()
        {
            if (_animationClock.Controller != null)
            {
                _animationClock.Controller.Stop();
                _animationClock.Controller.Remove();
            }
            if (_axisAnimation != null)
            {
                _axisRotation.BeginAnimation(AxisAngleRotation3D.AngleProperty, null);
            }
        }
        #endregion

        #region Methods
        public static void PreloadImages() {}

        public void Rotate()
        {
            _axisRotation.Angle--;
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            _grid.Arrange(new Rect(finalSize));
            return finalSize;
        }

        protected override Visual GetVisualChild(int index)
        {
            return _grid;
        }

        protected override Size MeasureOverride(Size availableSize)
        {
            _grid.Measure(availableSize);
            return _grid.DesiredSize;
        }
        #endregion
    }

    internal class Sphere
    {
        #region Fields
        private bool _changed;
        private int _latitude;
        private int _longitude;
        private MeshGeometry3D _mesh;
        private double _radius;
        #endregion

        #region Constructors
        public Sphere()
        {
            _latitude = 24;
            _longitude = 48;
            _radius = 1.0;
            _changed = true;
        }
        #endregion

        #region Properties
        public int Latitude
        {
            set
            {
                if (value != _latitude)
                {
                    _latitude = value;
                    _changed = true;
                }
            }
        }

        public int Longitude
        {
            set
            {
                if (value != _longitude)
                {
                    _longitude = value;
                    _changed = true;
                }
            }
        }

        public double Radius
        {
            set
            {
                if (value != _radius)
                {
                    _radius = value;
                    _changed = true;
                }
            }
        }

        public MeshGeometry3D Mesh
        {
            get
            {
                if (_changed)
                {
                    CreateMesh();
                    _changed = false;
                }

                return _mesh;
            }
        }
        #endregion

        #region Methods
        private void CreateMesh()
        {
            _mesh = new MeshGeometry3D();

            var latTheta = 0.0;
            var latDeltaTheta = Math.PI / _latitude;
            var lonDeltaTheta = 2.0 * Math.PI / _longitude;

            var origin = new Point3D(0, 0, 0);

            // Order of vertex creation:
            //  - For each latitude strip (y := [+radius,-radius] by -increment)
            //      - start at (-x,y,0)
            //      - For each longitude line (CCW about +y ... meaning +y points out of the paper)
            //          - generate vertex for latitude-longitude intersection

            // So if you have a 2x1 texture applied to this sphere:
            //      +---+---+
            //      | A | B |
            //      +---+---+
            // A camera pointing down -z with up = +y will see the "A" half of the texture.
            // "A" is considered to be the front of the sphere.

            for (int lat = 0; lat <= _latitude; lat++)
            {
                double v = (double)lat / _latitude;
                double y = _radius * Math.Cos(latTheta);
                double r = _radius * Math.Sin(latTheta);

                if (lat == _latitude - 1)
                {
                    latTheta = Math.PI; // Close the gap in case of precision error
                }
                else
                {
                    latTheta += latDeltaTheta;
                }

                double lonTheta = Math.PI;

                for (int lon = 0; lon <= _longitude + 1; lon++)
                {
                    double u = (double)lon / _longitude;
                    double x = r * Math.Cos(lonTheta);
                    double z = r * Math.Sin(lonTheta);
                    if (lon == _longitude - 1)
                    {
                        lonTheta = Math.PI; // Close the gap in case of precision error
                    }
                    else
                    {
                        lonTheta -= lonDeltaTheta;
                    }

                    Point3D p = new Point3D(x, y, z);
                    Vector3D norm = p - origin;

                    _mesh.Positions.Add(p);
                    _mesh.Normals.Add(norm);
                    _mesh.TextureCoordinates.Add(new Point(u, v));

                    if (lat != 0 && lon != 0)
                    {
                        // The loop just created the bottom right vertex (lat * (longitude + 1) + lon)
                        //  (the +1 comes because of the extra vertex on the seam)
                        // We only create panels when we're at the bottom-right vertex
                        //  (bottom-left, top-right, top-left have all been created by now)
                        //
                        //          +-----------+ x - (longitude + 1)
                        //          |           |
                        //          |           |
                        //      x-1 +-----------+ x

                        int bottomRight = lat * (_longitude + 1) + lon;
                        int bottomLeft = bottomRight - 1;
                        int topRight = bottomRight - (_longitude + 1);
                        int topLeft = topRight - 1;

                        // Wind counter-clockwise
                        _mesh.TriangleIndices.Add(bottomLeft);
                        _mesh.TriangleIndices.Add(topRight);
                        _mesh.TriangleIndices.Add(topLeft);

                        _mesh.TriangleIndices.Add(bottomRight);
                        _mesh.TriangleIndices.Add(topRight);
                        _mesh.TriangleIndices.Add(bottomLeft);
                    }
                }
            }
        }
        #endregion
    }
}